package md.system;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import md.cm.base.Person;
import md.cm.unit.Division;
import md.cm.unit.Office;
import md.cm.unit.Unit;
import org.fjsei.yewu.filter.UserBase;
import org.fjsei.yewu.filter.Uunode;
import org.fjsei.yewu.security.JwtUser;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.*;

//JPA实体类上加组合唯一索引https://blog.csdn.net/qq_29663071/article/details/80092538
//implements是graphQL-java套JPA用；服务端按照接口interface的字段,自动从Entity提取返回给协议执行器。
//User和其接口类Person按照名字一个对一个，这些*.graphql配置文件内不支持交叉使用,还要和JPA部分代码也保持一致。
//?普通的Java类并不在spring管理下，不能使用spring注入的service类。

/**后端应用系统用户：
 * 如何认证某个人是该单位/机构的人来开账户？他是某个监察机构的某一个地域的办事员(监察几类角色)，离职注销管理。
 * 用户都是Person个人, 用户不一定都一定归属检验检测监察机构的。一个人允许多个账户(用户)。
 * SDN报检用户属于某个单位{部门或科室可以省略不设定}。
 * 检验机构组织架构：部门+科室+用户。
 * 一个用户User只能关联唯一一个Unit单位。不允许跨单位，一个人Person允许多个单位分别设置User。
 * 特检院一个User只能挂接在最多一个部门，最多一个科室底下。不允许跨科室，跨部门，领导直接挂接在Unit不区分部门或科室。
 * 在特检院上班的一个职工，允许有开通多个User{比如一个User在省院新技术中心+另外一个User在泉州机器人}。
 * Person是自外部系统的维护数据源，避免没有它在就无法建用户，允许User没有Person，但是前端依赖于个人真实姓名的显示,所以尽量设置。
 * Hibernate缓存及查询策略  https://blog.csdn.net/weixin_40773848/article/details/112325949
 * */
@NoArgsConstructor
@Getter
@Setter
@Entity
@Table(name = "USERS",
        uniqueConstraints={@UniqueConstraint(columnNames={"username"})}
)
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region ="Slow")
public class User implements UserBase, Uunode {
    //注意id可能带来麻烦，数据库重整，可seq却从小开始，报唯一性约束错！select user_seq.nextval from dual;
    //若加@SequenceGenerator()旧表可修改initialValue到旧的表最大ID值，ID最多64位，就是19个数字的字符串，相当于说是无限大的。
    //不经过SequenceGenerator人工导入数据引起问题：后续@GeneratedValue若是遇见已经存在id就失败。尽量共用sequenceName，物理数据库设置sequence的next_val；
    //总之：id字段的稳定性非常强了，就算多个系统之间也是可能接受直接常用本id字段进行互通使用，也可能不需要校对其它关键字：用户感官上明确地关键描述字段。
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;

    //这里的@Size注解：1个中文字符也算1作个char的。
    /**本平台的登录页面，要求输入账户名，避免直接使用个人姓名，增加安全性。 zeebe流程引擎参考这个字段；
     * 手写输入账户名字，手机电话号码，微信号，QQ号，电子邮件名不超过30个字符的。
     * 流程引擎带来的限制 该字段必须符合正则 [a-zA-Z0-9]+
     * 字段的唯一性必须确保： zeebe等 外部对接系统需要它:协商对接KEY。
     * 上面的id字段其实也很不容易变化的，除非系统重新导入数据，正常UUID在现今分布式关系数据库能够依赖数据备份和恢复就能长期维持存储的，几乎不会需要变更UUID id字段的，除非系统融合系统迁移手术。
     * */
    @Column(name = "USERNAME",  unique = true)
    @NotNull
    @Size(min = 2, max = 60)
    private String username;

    //@ColumnTransformer()实际用底层数据库内部FUNCTION fjsei.decrypt 比较查询。 password字段须改blob，程序Jdbc明文，数据库存储密文。
    @Size(min = 6, max = 128)
    private String password;

    @Deprecated
    @Size(min = 1, max = 20)
    private String firstname;
    @Deprecated
    @Size(min = 1, max = 20)
    private String lastname;

    @Size(min = 6, max = 70)
    private String email;

    //该用户是合法的？ 未审核/屏蔽用户。前端控制用于页面的。
    @Column(name = "ENABLED")
    @NotNull
    private Boolean enabled=true;

    //这个字段代表密码更新，token该失效了。本字段不能比Now()超前。
    @Column(name = "LASTPASSWORDRESETDATE")
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastPasswordResetDate;

    /**必须为账户都设置权限，否则graphQL这层就无法访问后端数据。
    //这里维护多对多关系(JPA自动生成一个中间维护联系的关联表)，对方的实体仅仅说明我方的字段名。
    //设置小心，故障：hibernate.LazyInitializationException: failed to lazily initialize;
    //分页查询显示的，FetchType.EAGER 实际比FetchType.LAZY 也慢不了多少的，EAGER导致fetch join的一次查询结果集的行数量由于集合笛卡尔积很可能暴涨，两个集合关联性等因素影响；
    //一般graphQL遇FetchType.EAGER也会利用left outer join。就像是@EntityGraph它也会搞的fetch join;　
    //用.EAGER只要关联查User就必然多出1个sql;若只是查User.id采用.EAGER就不会多出sql;
     Set<Authority>若是换成List就会导致启动失败hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags
     Lazy导致前端无法登陆。 FetchType.EAGER只有这么一个地方采用的，其它代码都没有。
     角色 ！= 权限管理 ，！=用户组:窗口客服中心， 归属部门本科室 归属单位本公司，还是申请单实体的发起人，报告的授权编辑校核人。
    不能把Set<Authority>改成AuthorityName_Enum[]数组，JPA存储不好处理。再者判定有没有某一个角色之一？CRDB物理层支持？
     */
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(  name = "USER_AUTHORITY",
            joinColumns = {@JoinColumn(name = "USER_ID", referencedColumnName = "ID")},
            inverseJoinColumns = {@JoinColumn(name = "AUTHORITY_ID", referencedColumnName = "ID")})
    @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "Fast")
    private Set<Authority> authorities;

    /**用户本人：不建议搞出公司账户的概念存在， 账户密码应当直接归属公司员工。
     * User首先必须是Person, 账户密码是在个人掌控; User有Person但是不一定有Unit关联。
     * 一个Person个人可能在本平台有2个身份的账户就是User可多个的=形同兼职。一个Person允许在同一个Unit下有2个部门每个部门单独一个User账户。
     * */
    @ManyToOne(fetch= FetchType.LAZY)
    private Person person;
    /**就职单位是：属于哪一个单位机构,  使用单位是个人的也能开账户对应一个Unit；
     * User=账户=针对身份保持唯一标识；企业领导 只需要设置Unit字段
     * @ManyToOne缺省也是 EAGER
     * 监察机构: 省级？ 市级别？ 县级？ 镇级别；4个最少了也不配套Unit->Division->Office的层次啊。
     * 用户可能是检验员：归属某个特检院。User只能针对单一一个身份。一个Person可以有多个User,单人多账户不同身份的;
     * 个人用户不一定有归属Unit?有Person不一定都会有Unit关联对象。
     * */
    @ManyToOne
    private Unit unit;
    /**就职单位属于哪一个单位-部门
     * 特检院领导 不属于某一个Division
     * */
    @ManyToOne(fetch= FetchType.LAZY)
    private Division dep;
    /**就职单位属于哪一个单位-部门-科室
     * 部门领导 不属于某一个Office
     * 维保单位： 某公司(独立法人)->  分支机构 ->员工。？不需要Office;
     * 特检院的检验员若是Task负责人那么必须分配到某一个唯一部门唯一科室。
     * */
    @ManyToOne(fetch= FetchType.LAZY)
    private Office office;

    private String mobile;
    //头像
    private String  photoURL;

    //旧平台的
    private String oldAccount;
    //外部认证；
    private String  authType;
    private String  authName;


    /*这里堆栈:　在这个AsyncExecutionStrategy的这里public CompletableFuture<ExecutionResult> execute底下；
    graphql.execution.ExecutionStrategy在ExecutionStrategy文件当中的CompletableFuture<FieldValueInfo> resolveFieldWithInfo
     */
    //安全信息控制方式： 单个字段的。
    public Set<Authority> getAuthorities() {
        //当前SpringSecurity验证给出的用户Principal映射出来的User实体表ID。
        UUID curruser= JwtUser.getUserId();
        if(!id.equals(curruser))    //非超级用户，就不要看他人的权限列表
        {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            boolean hasMasterRole = (authentication != null)
                    && authentication.getAuthorities().stream()
                    .anyMatch(authority ->
                            authority instanceof SimpleGrantedAuthority
                                    && ((SimpleGrantedAuthority) authority).getAuthority().equals("ROLE_"+AuthorityName_Enum.Master.name()));
            //有超级用户 可以看所有人权限表
            if (hasMasterRole) {
                return authorities;
            }
            else return null;      //这样就切断了graphQL选择集，前端无法查询该字段也无法嵌套。
        }
        else
            return authorities;
    }
    //另外只给后端自身使用的替代函数：
    //本函数特殊！！
    public Set<Authority> heHasRoles() {
        return authorities;
    }

    //用不到?
    public void setAuthorities(Set<Authority> authorities) {
        this.authorities = authorities;
    }

    public Date getLastPasswordResetDate() {
        return lastPasswordResetDate;
    }

    public void setLastPasswordResetDate(Date lastPasswordResetDate) {
        this.lastPasswordResetDate = lastPasswordResetDate;
    }
    //QueryDsl查找contains(new User(contId))需要加;
    public  User(UUID id){
        this.id=id;
    }
    public  User(String name){
        this.username=name;
        this.lastPasswordResetDate =new Date();
        this.enabled=false;
    }

    //Entity这里的函数优先级比resolver要低？
    //对应同名字外模型User的约定enabled字段,graphQL都会来这里的,没带参数就是=null缺省;字段就如同函数那样。
    public Boolean getEnabled(Boolean isUsing) {
        //return  this.enabled==isUsing;
        return  this.enabled;
    }
    /*
    //Value Resolution/值解析ResolveFieldValue; resolver为objectType提供的内部函数，用以决定名为fieldName的字段的解析值。
    @PreAuthorize("hasRole('ADMIN')")
    public String dep(String dep){
        //如果前端就不给参数，那么这个输入参数是null
        if(dep!=null && !dep.equals(this.dep))      //不是修饰字段参数同样的预定的部门
            return null;
        return this.dep;
    }
    */

    /**用户登录成功之后，给auth接口前端返回定制的JSON信息: 部分关键字段。
     * 要保持返回信息的稳定性质，查询auth()不要每次都变化。
     *cloneAuth拷贝来的字段给前端，最终前端const {user, setUser} = useContext(UserContext)全部在user可见。
     * */
    //@PreAuthorize("hasRole('ADMIN')")
    @Deprecated
    public User cloneAuth(){
        User auth= new User(this.username);
        auth.setId(this.id);
        //auth.setEnabled(this.enabled);
        auth.setLastPasswordResetDate(this.lastPasswordResetDate);
        Set<Authority> outAuthorities=new HashSet<Authority>();
        this.authorities.stream().forEach(authority -> {
                    Authority  outAuthority = new Authority();
                    outAuthority.setName(authority.getName());
                    outAuthorities.add(outAuthority);
                } );
        //不同安全域需要的ROLE_XxYyy也不一样啊;　ROLE_cmnXxx通用的部分。
        //没啥意义，前端拿着权限做啥;
        auth.setAuthorities(outAuthorities);
        //直接设置unit 导致外面JSON.toJSONString()死循环？
        Unit unit=new Unit();
        unit.setId(this.unit.getId());
        auth.setUnit(unit);
        return auth;
    }

    //测试：嵌入对象没有独立的实体表，只有和使用者捆绑依附表记录，无法独立管理嵌入对象的设置(被复制的基本对象不能单独管理),只能跟随母实体一起修改。
    @Deprecated
    @ElementCollection
    //CRDB可以支持数据类型: ARRAY[简单型]的做法。 JPA不支持？nativeQuery
    @CollectionTable(name ="USER_PERMIS_ECL",joinColumns = @JoinColumn(name = "USER_ID"),
            indexes={@Index(columnList = "user_id") }
    )
    //[需要手动修改]没配置primary key导致CockroachDB自动默认的rowid INT8 DEFAULT unique_rowid(), #还有catalog等同数据库名seipf;
    private List<Phone> alias = Collections.emptyList();

    //组合形式的属性; @Embeddable只能复用代码，不涉及更多JPA;
    @Embeddable
    public class Phone {
        private String type;        //类别
        private String areaCode;    //区号区域
        private String number;      //电话号码
    }

    /*旧版本kickstart使用的是id()来为graphQL接口提供读取数据方法。新版Spring for graphQL不能用了只能默认getId()接口。
    Relay要求全局唯一的GlobalID 只能采取如下思路，单独提供一个ID映射方法：
        @Controller
        public class BookController {
            @SchemaMapping(typeName="Book", field="author")
            public Author getAuthor(Book book) {
            }
        }
    * */

/*    public String getId() {
        return Tool.toGlobalId(this.getClass().getSimpleName(), this.id);
    }*/

/*    public UUID getUuid(){
        return this.id;
    }*/

}





/*
函数式编程 apply 示例graphql-java-18.1-sources.jar/graphql/schema/PropertyFetchingImpl.java:124 或https://www.csdn.net/tags/NtTaMgxsMTU2MDAtYmxvZwO0O0OO0O0O.html
JPA 如何指定底层数据库的存储空间文件，分区文件。
@Column：(secondaryTable：如果此列不建在主表上（默认是主表），该属性定义该列所在从表的名字)
        主要用在主表，子表是自行定义，映射时使用两个类（集成关系），但为一个实体，保存到两个表的情况
@SecondaryTable(name = "xx", pkJoinColumns = @PrimaryKeyJoinColumn(name = "xid"))
　           ? ）extends从表名。
@Table属性：( catalog 和 sechema 属性指定据库名=一般不需要){} 像个域名create table door.newsei.Eqp {}
*/

//用哪个好？　@Column(size = 50)或@Size(max = 50)   https://www.cnblogs.com/ealenxie/p/10938371.html
/*修改数据源NativeQuery, 配置schema; {h-schema}占位符Hibernate语法；不用建同义词synonym
    @Query("select * from {h-schema}user", nativeQuery=true)
 spring.jpa.properties.hibernate.default_schema=my_schema
 字段加密@ColumnTransformer(read="AES_DECRYPT(PASSWORD,'x')",write="AES_ENCRYPT(?,'x')") 可移植性差，数据库函数或存储过程和物理数据库相关。
*/


/* @数据库修改脚本：
CREATE INDEX users_person_id_index ON public.users USING btree (person_id ASC);
CREATE INDEX users_unit_id_dep_id_office_id_index ON public.users USING btree (unit_id ASC, dep_id ASC, office_id ASC);
    中间表修改 ：默认是rowid INT8 DEFAULT unique_rowid(),
    ALTER TABLE user_permis_ecl ADD COLUMN id UUID NOT NULL DEFAULT gen_random_uuid();
    ALTER TABLE user_permis_ecl ALTER PRIMARY KEY USING COLUMNS (id);
    alter table user_permis_ecl  drop column rowid;
* */